using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using StardewModdingAPI.Events;
using StardewModdingAPI.Internal;

namespace StardewModdingAPI.Framework.Events;

/// <summary>An event wrapper which intercepts and logs errors in handler code.</summary>
/// <typeparam name="TEventArgs">The event arguments type.</typeparam>
internal class ManagedEvent<TEventArgs> : IManagedEvent
{
    /*********
    ** Fields
    *********/
    /// <summary>The mod registry with which to identify mods.</summary>
    protected readonly ModRegistry ModRegistry;

    /// <summary>The underlying event handlers.</summary>
    private readonly List<ManagedEventHandler<TEventArgs>> Handlers = [];

    /// <summary>A cached snapshot of the <see cref="Handlers"/> sorted by event priority, or <c>null</c> to rebuild it next raise.</summary>
    private ManagedEventHandler<TEventArgs>[]? CachedHandlers = [];

    /// <summary>The total number of event handlers registered for this events, regardless of whether they're still registered.</summary>
    private int RegistrationIndex;

    /// <summary>Whether handlers were removed since the last raise.</summary>
    private bool HasRemovedHandlers;

    /// <summary>Whether any of the handlers have a custom priority.</summary>
    private bool HasPriorities;


    /*********
    ** Accessors
    *********/
    /// <inheritdoc />
    public string EventName { get; }

    /// <inheritdoc />
    public bool HasListeners { get; private set; }


    /*********
    ** Public methods
    *********/
    /// <summary>Construct an instance.</summary>
    /// <param name="eventName">A human-readable name for the event.</param>
    /// <param name="modRegistry">The mod registry with which to identify mods.</param>
    public ManagedEvent(string eventName, ModRegistry modRegistry)
    {
        this.EventName = eventName;
        this.ModRegistry = modRegistry;
    }

    /// <summary>Add an event handler.</summary>
    /// <param name="handler">The event handler.</param>
    /// <param name="mod">The mod which added the event handler.</param>
    public void Add(EventHandler<TEventArgs> handler, IModMetadata mod)
    {
        lock (this.Handlers)
        {
            EventPriority priority = handler.Method.GetCustomAttribute<EventPriorityAttribute>()?.Priority ?? EventPriority.Normal;
            var managedHandler = new ManagedEventHandler<TEventArgs>(handler, this.RegistrationIndex++, priority, mod);

            this.Handlers.Add(managedHandler);
            this.CachedHandlers = null;
            this.HasListeners = true;
            this.HasPriorities |= priority != EventPriority.Normal;
        }
    }

    /// <summary>Remove an event handler.</summary>
    /// <param name="handler">The event handler.</param>
    public void Remove(EventHandler<TEventArgs> handler)
    {
        lock (this.Handlers)
        {
            // match C# events: if a handler is listed multiple times, remove the last one added
            for (int i = this.Handlers.Count - 1; i >= 0; i--)
            {
                if (this.Handlers[i].Handler != handler)
                    continue;

                this.Handlers.RemoveAt(i);
                this.CachedHandlers = null;
                this.HasListeners = this.Handlers.Count != 0;
                this.HasRemovedHandlers = true;
                break;
            }
        }
    }

    /// <summary>Raise the event and notify all handlers.</summary>
    /// <param name="args">The event arguments to pass.</param>
    public void Raise(TEventArgs args)
    {
        // skip if no handlers
        if (this.Handlers.Count == 0)
            return;

        // raise event
        foreach (ManagedEventHandler<TEventArgs> handler in this.GetHandlers())
        {
            Context.HeuristicModsRunningCode.Push(handler.SourceMod);

            try
            {
                handler.Handler(null, args);
            }
            catch (Exception ex)
            {
                this.LogError(handler, ex);
            }
            finally
            {
                Context.HeuristicModsRunningCode.TryPop(out _);
            }
        }
    }

    /// <summary>Raise the event and notify all handlers.</summary>
    /// <param name="invoke">Invoke an event handler. This receives the mod which registered the handler, and should invoke the callback with the event arguments to pass it.</param>
    public void Raise(Action<IModMetadata, Action<TEventArgs>> invoke)
    {
        // skip if no handlers
        if (this.Handlers.Count == 0)
            return;

        // raise event
        foreach (ManagedEventHandler<TEventArgs> handler in this.GetHandlers())
        {
            Context.HeuristicModsRunningCode.Push(handler.SourceMod);

            try
            {
                invoke(handler.SourceMod, args => handler.Handler(null, args));
            }
            catch (Exception ex)
            {
                this.LogError(handler, ex);
            }
            finally
            {
                Context.HeuristicModsRunningCode.TryPop(out _);
            }
        }
    }


    /*********
    ** Private methods
    *********/
    /// <summary>Log an exception from an event handler.</summary>
    /// <param name="handler">The event handler instance.</param>
    /// <param name="ex">The exception that was raised.</param>
    private void LogError(ManagedEventHandler<TEventArgs> handler, Exception ex)
    {
        handler.SourceMod.LogAsMod($"This mod failed in the {this.EventName} event. Technical details: \n{ex.GetLogSummary()}", LogLevel.Error);
    }

    /// <summary>Get cached copy of the sorted handlers to invoke.</summary>
    /// <remarks>This returns the handlers sorted by priority, and allows iterating the list even if a mod adds/removes handlers while handling it. This is debounced when requested to avoid repeatedly sorting when handlers are added/removed.</remarks>
    private ManagedEventHandler<TEventArgs>[] GetHandlers()
    {
        ManagedEventHandler<TEventArgs>[]? handlers = this.CachedHandlers;

        if (handlers == null)
        {
            lock (this.Handlers)
            {
                // recheck priorities
                if (this.HasRemovedHandlers)
                    this.HasPriorities = this.Handlers.Any(p => p.Priority != EventPriority.Normal);

                // sort by priority if needed
                if (this.HasPriorities)
                    this.Handlers.Sort();

                // update cache
                this.CachedHandlers = handlers = this.Handlers.ToArray();
                this.HasRemovedHandlers = false;
            }
        }

        return handlers;
    }
}
